/*
 * Category.java
 *
 *  
 */
package org.msb.finance.data;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.msb.finance.res.CoreMessages;

/**
 * The {@code Category} class represents a category that is used to categorize transactions within an account.
 * 
 * The {@code Category} instances must have a non-{@code null} name set and can contain sub-categories.
 * 
 * When new {@code Category} instance are created, they do not have a parent category, they are said to be detached. The
 * instances become attached when they are added as a sub-category of a parent category via the
 * {@link Cabinet#addCategory(Category, Category)} method.
 * 
 * In order to attach a {@code Category} instance, its name must be unique among all of its prospective siblings.
 * 
 * @see Cabinet#addCategory(Category, Category)
 * 
 * @author Marc Boudreau
 */
public class Category implements Comparable<Category> {

	/*
	 * Contains the name of this instance.
	 */
	private String name;

	/*
	 * A reference to the parent Category when this instance is linked.
	 */
	private Category parent;

	/*
	 * A map of name-to-Category pairs used to contain the sub-categories.
	 */
	private Map<String, Category> subCategories;

	/*
	 * This constructor is mandated by the JDO specification.
	 */
	@SuppressWarnings("unused")
	private Category() {
		this(null);
	}

	/**
	 * Creates a new {@code Category} instance that is not attached to any other {@code Category} objects.
	 * 
	 * @param name
	 *            A {@link String} object containing the name of the category.
	 */
	public Category(String name) {
		this.name = name;
		subCategories = new HashMap<String, Category>();
	}

	/**
	 * Returns the name of this {@code Category} object.
	 * 
	 * @return The name as a {@link String} object.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Returns a full name for this {@code Category} object that is based on the entire hierarchy. Simply put the full
	 * name consists of the name of each ancestor of this instance separated by a colon (:) and finally this instance's
	 * name.
	 * 
	 * @return A {@link String} object containing the full name.
	 */
	public String getFullName() {
		if (null != parent) {
			String parentFullName = parent.getFullName();
			if (null != parentFullName) {
				return parent.getFullName()
						+ CoreMessages.getString("org.msb.finance.data.Category.fullNameSeparator") + getName(); //$NON-NLS-1$
			}
		}

		return getName();
	}

	/*
	 * Sets the name of this {@code Category} object to the provided {@link String} parameter. If the new name is
	 * {@code null} a {@link NullPointerException} will be thrown.
	 * 
	 * If this instance is linked to a parent {@code Category} object, than the new name cannot be equal to any of this
	 * instance's sibling's name. If this is the case a {@link DuplicateNameException} will be thrown.
	 * 
	 * @param newName A {@link String} object containing the new name to set for this instance.
	 */
	@SuppressWarnings("null")
	void setName(String newName) {
		assert null == parent : CoreMessages.getString("org.msb.finance.data.Category.setNameAssertionMsg"); //$NON-NLS-1$
		assert null != newName : CoreMessages.getString("org.msb.finance.data.Category.setNameAssertion2Msg"); //$NON-NLS-1$

		if (null == parent && null != newName) {
			name = newName;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		} else if (null != obj && obj.getClass() == Category.class) {
			return name.equals(((Category) obj).name);
		}

		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo(Category o) {
		if (this == o) {
			return 0;
		}

		assert null != name && null != o.name : CoreMessages
				.getString("org.msb.finance.data.Category.compareToAssertionMsg"); //$NON-NLS-1$

		return name.compareTo(o.name);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return name.hashCode() ^ Category.class.hashCode();
	}

	/*
	 * Adds the provided {@link Category} instance as a child of this instance. The name of the provided
	 * {@link Category} instance must be unique among all of this instances children.
	 * 
	 * @param category
	 * 
	 * @throws DuplicateNameException
	 */
	boolean addSubCategory(Category category) {
		/*
		 * If a category group is not provided, this method cannot continue.
		 */
		if (null == category) {
			return false;
		}

		synchronized (subCategories) {
			if (subCategories.containsKey(category.getName())) {
				return false;
			}

			subCategories.put(category.getName(), category);
			category.parent = this;
		}

		return true;
	}

	/*
	 * 
	 */
	boolean removeFromParent() {
		if (null == parent) {
			return false;
		}

		return parent.removeSubCategory(this);
	}

	/*
	 * 
	 * @param category @return
	 */
	private boolean removeSubCategory(Category category) {
		synchronized (subCategories) {
			return null != subCategories.remove(category.getName());
		}
	}

	/*
	 * 
	 */
	boolean rename(String newName) {
		boolean result = true;

		if (null != parent) {
			result = parent.renameSubCategory(this, newName);
		}

		name = newName;

		return result;
	}

	/*
	 * @param newName
	 * 
	 */
	private boolean renameSubCategory(Category category, String newName) {
		/*
		 * If renaming to the same value, there's no need to go through all of this code.
		 */
		if (newName.equals(category.getName())) {
			return true;
		}

		synchronized (subCategories) {
			if (subCategories.containsKey(newName)) {
				return false;
			}

			subCategories.remove(category.getName());
			subCategories.put(newName, category);
		}

		return true;
	}

	/**
	 * Provides an {@link Iterator} object that will iterate over each sub-category in their natural order.
	 * 
	 * @return An {@link Iterator} of {@code Category} objects.
	 */
	public Iterator<Category> getSubCategories() {
		synchronized (subCategories) {
			return subCategories.values().iterator();
		}
	}

	/**
	 * Returns the number of {@code Category} objects contained directly by this instance. The count does not include
	 * any descendants that are not direct children of this instance.
	 * 
	 * @return The sub-category count as an {@code int} value.
	 */
	public int getSubCategoryCount() {
		synchronized (subCategories) {
			return subCategories.size();
		}
	}
}
